﻿//-----------------------------------------------------------------------
// <copyright file="Mp3MediaStreamSource.cs" company="Larry Olson">
// (c) Copyright Larry Olson.
// This source is subject to the Microsoft Public License (Ms-PL)
// See http://code.msdn.microsoft.com/ManagedMediaHelpers/Project/License.aspx
// All other rights reserved.
// </copyright>
//-----------------------------------------------------------------------

[module: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming",
    "CA1709:IdentifiersShouldBeCasedCorrectly",
    Scope = "type",
    Target = "Media.Mp3MediaStreamSource",
    MessageId = "Mp",
    Justification = "Mp is not a two letter acyonym but is instead part of Mp3")]

namespace Media
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Windows.Media;
    using System.Windows.Threading;
    using MediaParsers;
    using MichMan.Utilities;
    using System.Threading;
    using System.Diagnostics;

    public static class StreamHelpers
    {
        public static long BytesAvailable(this Stream stream)
        {
            if (stream is StreamingStream)
            {
                return ((StreamingStream)stream).BytesAvailable;
            }
            return stream.Length - stream.Position;
        }

        public static bool IsComplete(this Stream stream)
        {
            return stream.BytesAvailable() == stream.Length - stream.Position;
        }
    }

    /// <summary>
    /// A Simple MediaStreamSource which can play back MP3 streams from
    /// beginning to end.
    /// </summary>
    public class Mp3MediaStreamSource : MediaStreamSource
    {
        /// <summary>
        ///  ID3 version 1 tags are 128 bytes at the end of the file.
        ///  http://www.id3.org/ID3v1
        /// </summary>
        private const int Id3Version1TagSize = 128;

        /// <summary>
        /// The Mp3 stream being played back.
        /// </summary>
        private Stream audioStream;

        /// <summary>
        /// Description of the Mp3 Stream being played back which includes the
        /// MpegLayer3WaveFormat structure serialized out as a string of hex 
        /// characters.
        /// </summary>
        private MediaStreamDescription audioStreamDescription;

        /// <summary>
        ///  The position in the stream where the current MpegFrame starts.
        ///  For purposes of this code, the frame starts with the header and
        ///  not after the header.
        /// </summary>
        private long currentFrameStartPosition;
        private long firstFrameStartPosition;

        /// <summary>
        ///  The size of the MpegFrame in Bytes.
        /// </summary>
        private int currentFrameSize;

        /// <summary>
        /// The number of bits per second the raw audio is compressed into.
        /// </summary>
        private int currentBitrate;

        /// <summary>
        ///  Initializes a new instance of the Mp3MediaStreamSource class.
        /// </summary>
        /// <param name="audioStream">
        /// Seekable stream containing Mp3 data
        /// </param>
        public Mp3MediaStreamSource(Stream audioStream)
        {
            if (audioStream == null)
            {
                Debugger.Break();
            }
            this.audioStream = audioStream;
        }

        /// <summary>
        /// Gets the MpegLayer3WaveFormat structure which represents this Mp3 file.
        /// </summary>
        public MpegLayer3WaveFormat MpegLayer3WaveFormat { get; private set; }

        Timer Timer { get; set; }
        bool _opening = false;
        /// <summary>
        /// Parses the passed in MediaStream to find the first frame and signals
        /// to its parent MediaElement that it is ready to begin playback by calling
        /// ReportOpenMediaCompleted.
        /// </summary>
        object locker = new object();
        protected override void OpenMediaAsync()
        {
            this.Timer = new Timer(s =>
                {
                    lock (locker)
                    {
                        if (audioStream.BytesAvailable() < 4092 || _opening)
                        {
                            return;
                        }
                        try
                        {
                            _opening = true;
                            if (!OpenMediaSync())
                            {
                                audioStream.Position = 0;
                                _opening = false;
                            }
                            else
                            {
                                this.Timer.Change(Timeout.Infinite, Timeout.Infinite);
                                this.Timer = null;
                            }
                        }
                        catch
                        {
                        }
                    }
                }, this, 100, 100);
        }


        protected bool OpenMediaSync()
        {
            // Initialize data structures to pass to the Media pipeline via the MediaStreamSource
            Dictionary<MediaSourceAttributesKeys, string> mediaSourceAttributes = new Dictionary<MediaSourceAttributesKeys, string>();
            Dictionary<MediaStreamAttributeKeys, string> mediaStreamAttributes = new Dictionary<MediaStreamAttributeKeys, string>();
            List<MediaStreamDescription> mediaStreamDescriptions = new List<MediaStreamDescription>();
            MpegFrame mpegLayer3Frame = null;

            // Pull in the audio stream until we find the data.
            long limit = audioStream.BytesAvailable(); // This should at least be 4092.
            int result;
            while (true)
            {
                if (limit == 0)
                {
                    if (audioStream.Position == audioStream.Length)
                    {
                        // We've gone through the whole file and have not found it.
                        throw new InvalidOperationException("Cannot find first MPeg frame.");
                    }
                    // We didn't find it.  Try again later.
                    Debug.WriteLine("Didn't find it in the first " + audioStream.Position.ToString() + " bytes.  Try again later.");
                    return false;
                }

                // Pull in the entire Audio stream.
                byte[] audioData = new byte[Math.Min(limit, 4096)];
                // byte[] audioData = new byte[this.audioStream.Length];
                long readStart = audioStream.Position;
                if (audioData.Length != this.audioStream.Read(audioData, 0, audioData.Length))
                {
                    throw new IOException("Could not read in the AudioStream");
                }
                limit -= audioData.Length;
                // Find the syncpoint of the first MpegFrame in the file.
                result = BitTools.FindBitPattern(audioData, new byte[2] { 255, 240 }, new byte[2] { 255, 240 });
                if (result >= 0)
                {
                    result = result + (int)readStart;
                    this.audioStream.Position = result;
                    // We found it.  But we need a little more data than that.
                    if (audioStream.BytesAvailable() < 4)
                    {
                        return false;
                    }

                    // Mp3 frame validity check.
                    mpegLayer3Frame = new MpegFrame(this.audioStream);
                    if (mpegLayer3Frame.FrameSize <= 0)
                    {
                        // This is likely a non-frame.  Something in the before-frames part of the file that is not 
                        // actually a frame, but kind of looks like one.

                        // If we wanted to be paranoid, we could check that the next expected frame is actually a frame.
                        continue;
                    }
                    break;
                }
                // Back up in the stream so as to not miss a broken frame.
                this.audioStream.Position -= 1024;
            }

            // Initialize the Mp3 data structures used by the Media pipeline with state from the first frame.
            WaveFormatExtensible wfx = new WaveFormatExtensible();
            this.MpegLayer3WaveFormat = new MpegLayer3WaveFormat();
            this.MpegLayer3WaveFormat.WaveFormatExtensible = wfx;

            this.MpegLayer3WaveFormat.WaveFormatExtensible.FormatTag = 85;
            this.MpegLayer3WaveFormat.WaveFormatExtensible.Channels = (short)((mpegLayer3Frame.Channels == Channel.SingleChannel) ? 1 : 2);
            this.MpegLayer3WaveFormat.WaveFormatExtensible.SamplesPerSec = mpegLayer3Frame.SamplingRate;
            this.MpegLayer3WaveFormat.WaveFormatExtensible.AverageBytesPerSecond = mpegLayer3Frame.Bitrate / 8;
            this.MpegLayer3WaveFormat.WaveFormatExtensible.BlockAlign = 1;
            this.MpegLayer3WaveFormat.WaveFormatExtensible.BitsPerSample = 0;
            this.MpegLayer3WaveFormat.WaveFormatExtensible.Size = 12;

            this.MpegLayer3WaveFormat.Id = 1;
            this.MpegLayer3WaveFormat.BitratePaddingMode = 0;
            this.MpegLayer3WaveFormat.FramesPerBlock = 1;
            this.MpegLayer3WaveFormat.BlockSize = (short)mpegLayer3Frame.FrameSize;
            this.MpegLayer3WaveFormat.CodecDelay = 0;

            mediaStreamAttributes[MediaStreamAttributeKeys.CodecPrivateData] = this.MpegLayer3WaveFormat.ToHexString();
            this.audioStreamDescription = new MediaStreamDescription(MediaStreamType.Audio, mediaStreamAttributes);

            mediaStreamDescriptions.Add(this.audioStreamDescription);

            this.currentFrameStartPosition = result;
            this.firstFrameStartPosition = result;
            this.currentFrameSize = mpegLayer3Frame.FrameSize;
            this.currentBitrate = mpegLayer3Frame.Bitrate;

            long totalFrames = (this.audioStream.Length - result) / (this.currentFrameSize + 4);
            double secondsPerFrame = (double)(currentFrameSize * 8) / (double)this.currentBitrate;
            double totalTimeInSeconds = totalFrames * secondsPerFrame;

            mediaSourceAttributes[MediaSourceAttributesKeys.Duration] = TimeSpan.FromSeconds(totalTimeInSeconds).Ticks.ToString(CultureInfo.InvariantCulture);
            mediaSourceAttributes[MediaSourceAttributesKeys.CanSeek] = "1";

            // Report that the Mp3MediaStreamSource has finished initializing its internal state and can now
            // pass in Mp3 Samples.
            this.ReportOpenMediaCompleted(mediaSourceAttributes, mediaStreamDescriptions);

            return true;
        }

        /// <summary>
        /// Parses the next sample from the requested stream and then calls ReportGetSampleCompleted
        /// to inform its parent MediaElement of the next sample.
        /// </summary>
        /// <param name="mediaStreamType">
        /// Should always be Audio for this MediaStreamSource.
        /// </param>
        int numSamples = 0;
        protected override void GetSampleAsync(MediaStreamType mediaStreamType)
        {
            Interlocked.Increment(ref numSamples);
            // Debug.WriteLine("Requesting a sample.  Needed: " + numSamples);

            if (this.currentFrameStartPosition + this.currentFrameSize + Id3Version1TagSize >= this.audioStream.Length)
            {
                Debug.WriteLine("Sending LAST sample.");
                // If you are near the end of the file, return a null stream, which
                // tells the MediaStreamSource and MediaElement to close down.
                MediaStreamSample audioSample = new MediaStreamSample(
                    this.audioStreamDescription,
                    null,
                    0,
                    0,
                    0,
                    new Dictionary<MediaSampleAttributeKeys, string>());
                this.ReportGetSampleCompleted(audioSample);
                if (this.Timer != null)
                {
                    Debug.WriteLine("Done playing media!");
                    this.Timer.Change(Timeout.Infinite, Timeout.Infinite);
                    this.Timer = null;
                }
            }
            else
            {
                if (!GetSample(mediaStreamType))
                {
                    if (Timer == null)
                    {
                        this.Timer = new Timer(s =>
                        {
                            if (GetSample(mediaStreamType))
                            {
                                this.Timer.Change(Timeout.Infinite, Timeout.Infinite);
                            }
                        }, this, 100, 100);
                    }
                    else
                    {
                        Timer.Change(100, 100);
                    }
                }
                else
                {
                    if (Timer != null)
                    {
                        Timer.Change(Timeout.Infinite, Timeout.Infinite);
                    }
                }
            }
        }

        protected bool GetSample(MediaStreamType mediaStreamType)
        {
            long bytesAvailable = this.audioStream.BytesAvailable();
            long framesAvailable = bytesAvailable / (this.currentFrameSize + 4);
            double secondsPerFrame = (double)(currentFrameSize * 8) / (double)this.currentBitrate;
            double timeBufferedInSeconds = framesAvailable * secondsPerFrame;
            int bufferLength = 2 * AudioBufferLength;

            if (timeBufferedInSeconds * 1000 < bufferLength && !audioStream.IsComplete())
            {
                Debug.WriteLine("Time Avialable: " + timeBufferedInSeconds * 1000 + " Needed: " + bufferLength);
                double progress = (double)timeBufferedInSeconds * 1000 / (double)(bufferLength);
                // Debug.WriteLine("Report Sample Progress: " + progress);
                this.ReportGetSampleProgress(progress);
                return false;
            }

            // Debug.WriteLine("Sending a sample.  Needed: " + numSamples);
            // We have the data.  Stop the timer.
            GetSampleSync(mediaStreamType);
            return true;
        }

        protected void GetSampleSync(MediaStreamType mediaStreamType)
        {
            try
            {
                // The time at which a sample should be rendered as measured in 100 nanosecond increments
                long curFrame = this.currentFrameStartPosition / (this.currentFrameSize + 4);
                double secondsPerFrame = (double)(currentFrameSize * 8) / (double)this.currentBitrate;
                double timestampInSeconds = curFrame * secondsPerFrame;
                long timestamp = (long)(timestampInSeconds * 10000000.0);

                // Common case. Return the next sample in the stream and find the
                // one after it.
                MediaStreamSample audioSample = new MediaStreamSample(
                    this.audioStreamDescription,
                    this.audioStream,
                    this.currentFrameStartPosition,
                    this.currentFrameSize,
                    timestamp,
                    new Dictionary<MediaSampleAttributeKeys, string>());

                if (Interlocked.Decrement(ref numSamples) < 0)
                {
                    // Another thread is sending the sample.  Don't do anything.
                    Interlocked.Increment(ref numSamples);
                    return;
                }
                else
                {
                    // We got the lock on a sample.  Report the sample.
                    this.ReportGetSampleCompleted(audioSample);
                }

                MpegFrame nextFrame = new MpegFrame(this.audioStream);
                if (nextFrame.Version == 1 && nextFrame.Layer == 3)
                {
                    this.currentFrameStartPosition = this.audioStream.Position - 4;
                    this.currentFrameSize = nextFrame.FrameSize;
                }
                else
                {
                    throw new InvalidOperationException("Frame Is Not MP3");
                }
            }
            catch
            {
            }
        }

        /// <summary>
        ///  TODO FILL ME IN LATER
        /// </summary>
        protected override void CloseMedia()
        {
//            this.audioStream.Close();
        }

        /// <summary>
        ///  TODO FILL ME IN LATER
        /// </summary>
        /// <param name="diagnosticKind">
        ///  TODO FILL ME IN LATER . . .
        /// </param>
        protected override void GetDiagnosticAsync(MediaStreamSourceDiagnosticKind diagnosticKind)
        {
            /*
            switch (diagnosticKind)
            {
                case MediaStreamSourceDiagnosticKind.BufferLevelInBytes:
                case MediaStreamSourceDiagnosticKind.BufferLevelInMilliseconds:
            }
            // ReportGetDiagnosticCompleted();
             */
            throw new NotImplementedException();
        }

        /// <summary>
        /// <para>
        /// Effectively a Null-Op for when a MediaElement requests a seek at the beginning
        /// of the stream. This makes the stream semi-unseekable.
        /// </para>
        /// <para>
        /// In a fuller MediaStreamSource, the logic here would be to actually seek to
        /// the correct mpeg frame matching the seekToTime passed in.
        /// </para>
        /// </summary>
        /// <param name="seekToTime">
        ///  The time to seek to in nanosecond ticks.
        /// </param>
        protected override void SeekAsync(long seekToTime)
        {
            long totalFrames = (this.audioStream.Length - currentFrameStartPosition) / (this.currentFrameSize + 4);
            double secondsPerFrame = (double)(currentFrameSize * 8) / (double)this.currentBitrate;
            double totalTimeInSeconds = totalFrames * secondsPerFrame;

            double neededTimestampInSeconds = (double)seekToTime / 10000000.0;

            long neededFrame = (long)((double)totalFrames * neededTimestampInSeconds / totalTimeInSeconds);

            audioStream.Position = currentFrameStartPosition + (this.currentFrameSize + 4) * neededFrame;

            this.ReportSeekCompleted(seekToTime);
        }

        /// <summary>
        ///  TODO FILL ME IN LATER
        /// </summary>
        /// <param name="mediaStreamDescription">
        ///  TODO FILL ME IN LATER . . .
        /// </param>
        protected override void SwitchMediaStreamAsync(MediaStreamDescription mediaStreamDescription)
        {
            throw new NotImplementedException();
        }
    }
}
